/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package se.kth.lwr.jeat.core;

import java.util.*;
import java.util.Map.Entry;
import javax.measure.quantity.Quantity;
import javax.measure.unit.SI;
import org.jscience.physics.amount.Amount;

/**
 * Inventory is a class that contains inventory data (data on emissions and
 * consumptions) for a specific process. The class provides methods to easy
 * manipulate and access the inventory data.
 *
 *
 *
 * @author David
 */
public class Inventory implements Reportable {

    private InventoryMap<EmissionCategory> emissionsToWater;
    private InventoryMap<EmissionCategory> emissionsToAir;
    private InventoryMap<EmissionCategory> emissionsToSoil;
    private InventoryMap<ResourceCategory> resourceUse;
    private InventoryMap<EnergyCategory> energyUse;

    /**
     * private constructor
     */
    private Inventory() {
        emissionsToWater = new InventoryMap<>();
        emissionsToSoil = new InventoryMap<>();
        emissionsToAir = new InventoryMap<>();
        resourceUse = new InventoryMap<>();
        energyUse = new InventoryMap<>();



    }

    /**
     *
     * @return an Empty inventory without any data at all
     */
    public static Inventory emptyInventory() {
        Inventory retval = new Inventory();
        return retval;
    }

    //TODO would it be more consistant to let the inventory implement Inventoriable?
    /**
     *
     * @return this inventory as an {@link Inventoriable}
     */
    public Inventoriable asInventoriable() {
        return new InventoriableWrapper(this);
    }

    /**
     * Calculates the difference (D) between this inventory (X) and the argument
     * (Y) such as D = X - Y and returns a resulting Inventory (D). No changes
     * will affect X or Y.
     *
     * @param inventory the argument (Y)
     * @return an inventory the result (D)
     */
    public Inventory difference(Inventory inventory) {
        //calculate opposite argument
        Inventory result = inventory.multiply(Amount.ONE.opposite());
        //return sum of this inventory and opposit argument
        return merge(result);

    }

    //TODO Denna funktion går att effektivisera. Nu loopar vi genom alla
    //existerande categorier istället för att gå igenom dem som faktiskt håller
    //några världen.
    /**
     * Merges data from another inventory (Y) with this inventory (X) and
     * returns the resulting Inventory (R) such as R = X + Y. The operation do
     * not affect the values of X or Y. Be aware that this operation requires
     * that units of the data in X and Y are of same Dimensions. If this
     * requirement is not satisfied it will lead to an Exception.
     *
     * @param inventory the addition (Y)
     * @return A new Inventory that holds the sum of the two Inventories (R).
     */
    public Inventory merge(Inventory inventory) {
        Inventory retval = Inventory.emptyInventory();

        //merge emission
        for (EmissionCategory cat : EmissionCategory.CO2.getCategories()) {
            retval.addEmissionToWater(cat, this.getEmissionToWater(cat));
            retval.addEmissionToWater(cat, inventory.getEmissionToWater(cat));
            retval.addEmissionToAir(cat, this.getEmissionToAir(cat));
            retval.addEmissionToAir(cat, inventory.getEmissionToAir(cat));
            retval.addEmissionToSoil(cat, this.getEmissionToSoil(cat));
            retval.addEmissionToSoil(cat, inventory.getEmissionToSoil(cat));
        }

        //merge resource use
        for (ResourceCategory cat : ResourceCategory.WATER.getCategories()) {
            retval.addResourceUse(cat, this.getResourceUse(cat));
            retval.addResourceUse(cat, inventory.getResourceUse(cat));
        }

        //merge energy use
        for (EnergyCategory cat : EnergyCategory.ELECTRICAL.getCategories()) {
            retval.addEnergyUse(cat, this.getEnergyUse(cat));
            retval.addEnergyUse(cat, inventory.getEnergyUse(cat));
        }

        return retval;

    }

    /**
     *
     *
     * Multiplies all values in this inventory (X) with the given Amount (Y) and
     * returns a resulting Inventory (R) such as R = X * Y
     *
     * @param amount the multiplier (Y)
     * @return the resulting Inventory (R)
     */
    //TODO USE NEW TIMES METHOD IN INVENTORY MAP!
    public Inventory multiply(Amount<?> amount) {

        Inventory retval = Inventory.emptyInventory();
        for (EmissionCategory cat : emissionsToAir.keySet()) {
            retval.addEmissionToAir(cat, amount.times(getEmissionToAir(cat)));
        }
        for (EmissionCategory cat : emissionsToWater.keySet()) {
            retval.addEmissionToWater(cat, amount.times(getEmissionToWater(cat)));
        }
        for (EmissionCategory cat : emissionsToSoil.keySet()) {
            retval.addEmissionToSoil(cat, amount.times(getEmissionToSoil(cat)));
        }
        for (ResourceCategory cat : resourceUse.keySet()) {
            retval.addResourceUse(cat, amount.times(getResourceUse(cat)));
        }
        for (EnergyCategory cat : energyUse.keySet()) {
            retval.addEnergyUse(cat, amount.times(getEnergyUse(cat)));
        }
        return retval;
    }

    /**
     * Returns an Amount of a particular emission to Air
     *
     * @param category The {@link EmissionCategory} to return
     * @return An Amount of the emission emitted to the air
     */
    public Amount<? extends Quantity> getEmissionToAir(EmissionCategory category) {
        return emissionsToAir.getAmount(category);
    }

    /**
     * Adds an emission to Air of a specified {@link EmissionCategory} to this
     * Inventory.
     *
     * @param category The type of {@link EmissionCategory}
     * @param amount The Amount of the emission emitted to the Air
     */
    public void addEmissionToAir(EmissionCategory category, Amount<?> amount) {
        emissionsToAir.addAmount(category, amount);
    }

    /**
     * Delivers an unmodifiable Map view of the emissions to Air
     *
     * @return the emissions to air
     */
    public Map<EmissionCategory, Amount<? extends Quantity>> getEmissionsToAir() {
        return emissionsToAir.getMap();
    }

    /**
     * Add resource use of a specified {@link ResourceCategory} to this
     * Inventory.
     *
     * @param category The type of the resource to return
     * @param amount The Amount of the consumed Resource
     */
    public void addResourceUse(ResourceCategory category, Amount<?> amount) {
        resourceUse.addAmount(category, amount);
    }

    /**
     * Returns a resource use for a specified {@link ResourceCategory}
     *
     * @param category The type of resource use to add
     * @return An Amount of resource use
     */
    public Amount<?> getResourceUse(ResourceCategory category) {
        return resourceUse.getAmount(category);
    }

    /**
     * Delivers a unmodifiable Map view of the resource use
     *
     * @return the resource use
     */
    public Map<ResourceCategory, Amount<? extends Quantity>> getResourceUse() {
        return resourceUse.getMap();
    }

    /**
     * Returns an emission to soil for a specified {@link EmissionCategory}
     *
     * @param category The type of emission to return
     * @return An Amount emission to soil
     */
    public Amount<?> getEmissionToSoil(EmissionCategory category) {
        return emissionsToSoil.getAmount(category);
    }

    /**
     * Adds an emission to Soil of a specified  {@link EmissionCategory} to this
     * Inventory.
     *
     * @param category The type of emission to add
     * @param amount The Amount of the emission to add
     */
    public void addEmissionToSoil(EmissionCategory category, Amount<?> amount) {
        emissionsToSoil.addAmount(category, amount);
    }

    /**
     * Delivers a unmodifiable Map view of the emissions to soil
     *
     * @return the emissions to soil
     */
    public Map<EmissionCategory, Amount<? extends Quantity>> getEmissionsToSoil() {
        return emissionsToSoil.getMap();
    }

    /**
     * Adds an emission to water of a specified {@link EmissionCategory} to this
     * Inventory.
     *
     * @param category The type of emission to add
     * @param amount he Amount of the emission to add
     */
    public void addEmissionToWater(EmissionCategory category, Amount<?> amount) {
        emissionsToWater.addAmount(category, amount);
    }

    /**
     * Returns an Amount of an emission to Water for a specified {@link EmissionCategory}
     *
     * @param category The type of emission to get
     * @return An Amount of the emission
     */
    public Amount<?> getEmissionToWater(EmissionCategory category) {
        return emissionsToWater.getAmount(category);
    }

    /**
     * Delivers an unmodifiable Map view of the emissions to water
     *
     * @return the emissions to water
     */
    public Map<EmissionCategory, Amount<? extends Quantity>> getEmissionsToWater() {
        return Collections.unmodifiableMap(emissionsToWater.getMap());

    }

    /**
     * Adds the use of energy of a specified {@link EnergyCategory} to this
     * inventory
     *
     * @param category The type of energy used
     * @param amount The amount of energy used
     */
    public void addEnergyUse(EnergyCategory category, Amount<?> amount) {
        energyUse.addAmount(category, amount);
    }

    /**
     * Returns the Amount of energy use of a specified {@link EnergyCategory}
     *
     * @param category The type of energy to get
     * @return An Amount of a energy
     */
    public Amount<?> getEnergyUse(EnergyCategory category) {
        return energyUse.getAmount(category);
    }

    /**
     * Delivers an unmodifiable map view of the energy use
     *
     * @return the energy usage
     */
    public Map<EnergyCategory, Amount<? extends Quantity>> getEnergyUse() {
        return energyUse.getMap();
    }

    /**
     * Custom implementation of equals. An Inventory is considered to be equals
     * with another Inventory if all the data they carries are equals.
     */
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Inventory other = (Inventory) obj;
        if (!Objects.equals(this.emissionsToWater, other.emissionsToWater)) {
            return false;
        }
        if (!Objects.equals(this.emissionsToAir, other.emissionsToAir)) {
            return false;
        }
        if (!Objects.equals(this.emissionsToSoil, other.emissionsToSoil)) {
            return false;
        }
        if (!Objects.equals(this.resourceUse, other.resourceUse)) {
            return false;
        }
        if (!Objects.equals(this.energyUse, other.energyUse)) {
            return false;
        }
        return true;
    }

    //Used autogeneneration from the Netbeans editor with the Map fields
    public int hashCode() {
        int hash = 7;
        hash = 97 * hash + Objects.hashCode(this.emissionsToWater);
        hash = 97 * hash + Objects.hashCode(this.emissionsToAir);
        hash = 97 * hash + Objects.hashCode(this.emissionsToSoil);
        hash = 97 * hash + Objects.hashCode(this.resourceUse);
        hash = 97 * hash + Objects.hashCode(this.energyUse);
        return hash;
    }

//    public void dump() {
//        System.out.println("Emmissions to Air ->");
//
//        for (EmissionCategory c : emissionsToAir.keySet()) {
//            System.out.println(c.getName() + ": " + getEmissionToAir(c).to(SI.KILOGRAM));
//        }
//        System.out.println("Emmissions to Water ->");
//
//        for (EmissionCategory c : emissionsToWater.keySet()) {
//            System.out.println(c.getName() + ": " + getEmissionToWater(c).to(SI.KILOGRAM));
//        }
//
//        System.out.println("Emmissions to Soil ->");
//        for (EmissionCategory c : emissionsToSoil.keySet()) {
//            System.out.println(c.getName() + ": " + getEmissionToSoil(c));
//        }
//
//        System.out.println("Use of resources ->");
//        for (ResourceCategory c : resourceUse.keySet()) {
//            System.out.println(c.getName() + ": " + getResourceUse(c));
//        }
//
//        System.out.println("Use of power ->");
//        for (EnergyCategory c : energyUse.keySet()) {
//            System.out.println(c.getName() + ": " + getEnergyUse(c));
//        }
//    }
    /**
     *
     * @return a {@link Report} over the data in this inventory
     */
    @Override
    public Report getReport() {
        Report report = new Report();

        //report emission to air data
        if (!getEmissionsToAir().isEmpty()) {
            report.addRow("Emissions to air:");
            Set<Entry<EmissionCategory, Amount<? extends Quantity>>> keyValue = getEmissionsToAir().entrySet();
            for (Entry<EmissionCategory, Amount<? extends Quantity>> entry : keyValue) {
                report.addRow(entry.getKey(), entry.getValue(), null);
            }

        }
        //report water emissions
        if (!getEmissionsToWater().isEmpty()) {
            report.addRow("Emissions to water:");
            Set<Entry<EmissionCategory, Amount<? extends Quantity>>> keyValue = getEmissionsToWater().entrySet();
            for (Entry<EmissionCategory, Amount<? extends Quantity>> entry : keyValue) {
                report.addRow(entry.getKey(), entry.getValue(), null);
            }

        }

        //report soil emissions
        if (!getEmissionsToSoil().isEmpty()) {
            report.addRow("Emissions to soil:");
            Set<Entry<EmissionCategory, Amount<? extends Quantity>>> keyValue = getEmissionsToSoil().entrySet();
            for (Entry<EmissionCategory, Amount<? extends Quantity>> entry : keyValue) {
                report.addRow(entry.getKey(), entry.getValue(), null);
            }

        }

        //report resource use
        if (!getResourceUse().isEmpty()) {
            report.addRow("Resource use:");
            Set<Entry<ResourceCategory, Amount<? extends Quantity>>> keyValue = getResourceUse().entrySet();
            for (Entry<ResourceCategory, Amount<? extends Quantity>> entry : keyValue) {
                report.addRow(entry.getKey(), entry.getValue(), null);
            }

        }

        //report energy use
        if (!getEnergyUse().isEmpty()) {
            report.addRow("Energy use:");
            Set<Entry<EnergyCategory, Amount<? extends Quantity>>> keyValue = getEnergyUse().entrySet();
            for (Entry<EnergyCategory, Amount<? extends Quantity>> entry : keyValue) {
                report.addRow(entry.getKey(), entry.getValue(), null);
            }

        }

        return report;

    }

    /**
     * A class that wraps the inventory as an {@link Inventoriable}
     */
    private class InventoriableWrapper implements Inventoriable {

        public InventoriableWrapper(Inventory aThis) {
        }

        @Override
        public Inventory getInventory() {
            return Inventory.this;
        }
    }

    /**
     * A class that wraps a TreeMap for storage of inventory data. It provides
     * necessary methods for addition, multiplication and retrieval of inventory 
     * data.
     *
     * @param <K> The type of {@link Category} is used as key in this inventoryMap.
     *
     *
     */
    private class InventoryMap<K extends Category> {

        private TreeMap<K, Amount<? extends Quantity>> inventory;

        public InventoryMap() {
            inventory = new TreeMap<>();
        }
        
        /**Adds an amount of a specified Category to this Map
         * 
         * @param category category of the amount
         * @param amount the amount to add
         */
        //TODO omkastning ab exeption vid adition med okompatibla enheter
        //kunde vara bra för att ge tydligare
        //information om vad som gått snett 

        public void addAmount(K category, Amount<? extends Quantity> amount) {
            if (amount == null) {
                return;
            } else {
                if (inventory.containsKey(category) && inventory.get(category) != null) {
                    Amount<? extends Quantity> current = inventory.get(category);
                    inventory.put(category, current.plus(amount));
                } else {
                    inventory.put(category, amount);
                }
            }

        }

        /**
         * Multiply all values in the map with an amount
         *
         * @param amount
         */
        public void times(Amount amount) {
            for (Amount value : inventory.values()) {
                value.times(amount);
            }
        }

        /**
         *
         * @return an unmodifiable Map containing key, value pairs for this
         * inventoryMap.
         */
        public Map<K, Amount<? extends Quantity>> getMap() {
            return Collections.unmodifiableMap(inventory);
        }

        //Used autogeneration by NetBeans IDE
        @SuppressWarnings("unchecked")
        @Override
        //TO DO osäker på varför jag implementerat detta men förmodligen 
        //Hör det ihop med att vi vill kunna jämföra Inventories
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final InventoryMap<K> other = (InventoryMap<K>) obj;
            if (!Objects.equals(this.inventory, other.inventory)) {
                return false;
            }
            return true;
        }

        public int hashCode() {
            int hash = 7;
            hash = 29 * hash + Objects.hashCode(this.inventory);
            return hash;
        }
        
        /**
         * @return an amount of a specified {@link Category}
         * @param category the specified {@link Category}
         */
        public Amount<? extends Quantity> getAmount(K category) {
            return inventory.get(category);

        }
        
        /**
         * 
         * @return the keys of the Map
         */
        public Set<K> keySet() {
            return inventory.keySet();
        }
    }
}
